<?php

// +----------------------------------------------------------------------
// | WeChatDeveloper
// +----------------------------------------------------------------------
// | 版权所有 2014~2018 广州楚才信息科技有限公司 [ http://www.cuci.cc ]
// +----------------------------------------------------------------------
// | 官方网站: http://think.ctolog.com
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// +----------------------------------------------------------------------
// | github开源项目：https://github.com/zoujingli/WeChatDeveloper
// +----------------------------------------------------------------------

namespace Itdashu\SwoftThirdPartyService\Service\WeChat\Contracts;

use Itdashu\SwoftThirdPartyService\Service\WeChat\Exceptions\InvalidArgumentException;
use Itdashu\SwoftThirdPartyService\Service\WeChat\Exceptions\InvalidDecryptException;
use Itdashu\SwoftThirdPartyService\Service\WeChat\Exceptions\InvalidResponseException;
use Itdashu\SwoftThirdPartyService\Service\Base\DataArray;
use Itdashu\SwoftThirdPartyService\Service\Base\Tools;

/**
 * 微信通知处理基本类
 * Class BasicPushEvent
 * @package WeChat\Contracts
 */
class BasicPushEvent
{
  /**
   * 公众号APPID
   * @var string
   */
  protected $appId;

  /**
   * 公众号推送XML内容
   * @var string
   */
  protected $postXml;

  /**
   * 公众号推送加密类型
   * @var string
   */
  protected $encryptType;

  /**
   * 公众号的推送请求参数
   * @var DataArray
   */
  protected $input;

  /**
   * 当前公众号配置对象
   * @var DataArray
   */
  protected $config;

  /**
   * 公众号推送内容对象
   * @var DataArray
   */
  protected $receive;

  /**
   * 准备回复的消息内容
   * @var array
   */
  protected $message;

  /**
   * BasicPushEvent constructor.
   * @param array $options
   * @throws InvalidResponseException
   * @throws \Swoft\Exception\SwoftException
   */
  public function __construct(array $options)
  {
    if (empty($options['appid'])) {
      throw new InvalidArgumentException('Missing Config -- [appid]');
    }
    if (empty($options['appsecret'])) {
      throw new InvalidArgumentException('Missing Config -- [appsecret]');
    }
    if (empty($options['token'])) {
      throw new InvalidArgumentException('Missing Config -- [token]');
    }
    // 参数初始化
    $this->config = new DataArray($options);
//    $this->input = new DataArray($_REQUEST);
    $this->input = new DataArray(context()->getRequest()->input());
    $this->appId = $this->config->get('appid');
    // 推送消息处理
    if (strtoupper(context()->getRequest()->getMethod()) === 'POST') {
      $this->postXml = file_get_contents('php://input');
      $this->encryptType = $this->input->get('encrypt_type');
      if ($this->isEncrypt()) {
        if (empty($options['encodingaeskey'])) {
          throw new InvalidArgumentException('Missing Config -- [encodingaeskey]');
        }
        $prpCrypt = new PrpCrypt($this->config->get('encodingaeskey'));
        $result = Tools::xml2arr($this->postXml);
        $array = $prpCrypt->decrypt($result['Encrypt']);
        if ((int)$array[0] > 0) {
          throw new InvalidResponseException($array[1], $array[0]);
        }
        [$this->postXml, $this->appId] = [$array[1], $array[2]];
      }
      $this->receive = new DataArray(Tools::xml2arr($this->postXml));
    } elseif ($this->checkSignature() && strtoupper(context()->getRequest()->getMethod()) === 'GET') {
      @ob_clean();
      exit($this->input->get('echostr'));
    } else {
      throw new InvalidResponseException('Invalid interface request.', '0');
    }
  }

  /**
   * 消息是否需要加密
   * @return boolean
   */
  public function isEncrypt(): bool
  {
    return $this->encryptType === 'aes';
  }

  /**
   * 回复消息
   * @param array $data 消息内容
   * @param boolean $return 是否返回XML内容
   * @param boolean $isEncrypt 是否加密内容
   * @return string|null
   * @throws InvalidDecryptException|\Exception
   */
  public function reply(array $data = [], $return = false, $isEncrypt = false): string
  {
    $xml = Tools::arr2xml(empty($data) ? $this->message : $data);
    if ($isEncrypt || $this->isEncrypt()) {
      $prpCrypt = new PrpCrypt($this->config->get('encodingaeskey'));
      // 如果是第三方平台，加密得使用 component_appid
      $componentAppId = $this->config->get('component_appid');
      $appId = empty($componentAppId) ? $this->appId : $componentAppId;
      $array = $prpCrypt->encrypt($xml, $appId);
      if ($array[0] > 0) {
        throw new InvalidDecryptException('Encrypt Error.', '0');
      }
      [$timestamp, $encrypt] = [time(), $array[1]];
      $nonce = random_int(77, 999) * random_int(605, 888) * random_int(11, 99);
      $tmpArr = [$this->config->get('token'), $timestamp, $nonce, $encrypt];
      sort($tmpArr, SORT_STRING);
      $signature = sha1(implode($tmpArr));
      $format = '<xml><Encrypt><![CDATA[%s]]></Encrypt><MsgSignature><![CDATA[%s]]></MsgSignature><TimeStamp>%s</TimeStamp><Nonce><![CDATA[%s]]></Nonce></xml>';
      $xml = sprintf($format, $encrypt, $signature, $timestamp, $nonce);
    }
    if ($return) {
      return $xml;
    }
    @ob_clean();
    echo $xml;
    return '';
  }

  /**
   * 验证来自微信服务器
   * @param string $str
   * @return bool
   */
  private function checkSignature($str = ''): bool
  {
    $nonce = $this->input->get('nonce');
    $timestamp = $this->input->get('timestamp');
    $msgSignature = $this->input->get('msg_signature');
    $signature = empty($msgSignature) ? $this->input->get('signature') : $msgSignature;
    $tmpArr = [$this->config->get('token'), $timestamp, $nonce, $str];
    sort($tmpArr, SORT_STRING);
    return sha1(implode($tmpArr)) === $signature;
  }

  /**
   * 获取公众号推送对象
   * @param null|string $field 指定获取字段
   * @return array
   */
  public function getReceive($field = null): array
  {
    return $this->receive->get($field);
  }

  /**
   * 获取当前微信OPENID
   * @return string
   */
  public function getOpenid(): string
  {
    return $this->receive->get('FromUserName');
  }

  /**
   * 获取当前推送消息类型
   * @return string
   */
  public function getMsgType(): string
  {
    return $this->receive->get('MsgType');
  }

  /**
   * 获取当前推送消息ID
   * @return string
   */
  public function getMsgId(): string
  {
    return $this->receive->get('MsgId');
  }

  /**
   * 获取当前推送时间
   * @return integer
   */
  public function getMsgTime(): int
  {
    return $this->receive->get('CreateTime');
  }

  /**
   * 获取当前推送公众号
   * @return string
   */
  public function getToOpenid(): string
  {
    return $this->receive->get('ToUserName');
  }
}